package ch.jamme.impl;

import java.util.*;
import java.util.Map.*;

import org.jdom.*;

import ch.jamme.conf.*;
import ch.jamme.exception.*;
import ch.jamme.util.*;

class ObjectToXmlConvertor {

	protected MarshallerEngine marshallerEngine;
	
	protected MarshallerConfiguration configuration;
	protected FieldPrimitiveStrategy fieldPrimitiveStrategy;
	protected FieldBindingStrategy fieldBindingStrategy;
	protected FieldNamingStrategy fieldNamingStrategy;
	protected ObjectToStringConvertor objectToStringConvertor;
	protected ObjectIntrospector objectIntrospector;
	protected FieldSuppressionStrategy fieldSuppressionStrategy;
	protected ClassNormalizer classNormalizer;
	protected NamingConvention namingConvention;
	
	protected boolean isDebugEnabled = false;

	ObjectToXmlConvertor(MarshallerEngine marshallerEngine) {
		this.marshallerEngine = marshallerEngine;
		configuration = marshallerEngine.getConfiguration();
		fieldBindingStrategy = configuration.getFieldBindingStrategy();
		fieldPrimitiveStrategy = configuration.getFieldPrimitiveStrategy();
		fieldNamingStrategy = configuration.getFieldNamingStrategy();
		objectToStringConvertor = configuration.getObjectToStringConvertor();
		objectIntrospector = configuration.getObjectIntrospector();
		fieldSuppressionStrategy = configuration.getFieldSuppressionStrategy();
		classNormalizer = configuration.getClassNormalizer();
		namingConvention = configuration.getNamingConvention(); 
		isDebugEnabled = Logger.isDebugEnabledFor(this);
	}
	
	protected Element toElement(Object object) {
		Element rootElement = new Element(namingConvention.getRootTag(object));
		populateElement(rootElement, object, 1);
		return rootElement;
	}
	
	protected void processElement(Element parentElement, String elementName, Object object, Object parent) {
		Element processedElement = marshallerEngine.getParsedElement(elementName, object);
		if (processedElement == null) {
			int objectId = marshallerEngine.registerParsingObject(object, parent);
			processedElement = new Element(elementName);
			populateElement(processedElement, object, objectId);
		} else if (configuration.getMarshallStyle().checkLoops()) {
			processedElement = processNestedObject(elementName, object);
		}
		parentElement.addContent(processedElement);
	}
	
	protected final Element processNestedObject(String elementName, Object object) {
		Element processedElement = new Element(elementName);
		if (marshallerEngine.isChildOf(object, object)) {
			if (configuration.getMarshallStyle() == MarshallStyle.ERROR_ON_RECURSION) throw new RecursiveNestedObjectException(object);
		} else {
			populateElement(processedElement, object, null);
		}
		return processedElement;
	}
	
	protected void populateElement(Element element, Object object, Integer objectId) {
		if (configuration.getMarshallStyle().printObjectIds())
			element.setAttribute(namingConvention.getIdentifierAttribute(), Integer.toString(objectId));
		populateElement(element, object);
	}
	
	protected void populateElement(Element element, Object object) {
		Class<?> normalizedClass = classNormalizer.getNormalizedClass(object);
		addClassNameAttribute(element, normalizedClass);

		// Discovers on all the fields of the class to be marshalled ...
		Map<String, Object> fieldValues = objectIntrospector.introspect(fieldSuppressionStrategy, normalizedClass, object);
		
		// ... and iterate on them
		for (Entry<String, Object> fieldEntry : fieldValues.entrySet()) {
			Object value = fieldEntry.getValue();
			if (value != null) parseValue(element, normalizedClass, fieldEntry.getKey(), value, object);
		}
	}

	private void parseValue(Element element, Class<?> containingClass, String name, Object value, Object parent) {
		name = fieldNamingStrategy.translate(containingClass, name, true);
		if (isDebugEnabled) Logger.debug(this, "Marshalling field " + name + " of " + containingClass + " having value " + value);
		if (fieldBindingStrategy.bindAsAttribute(containingClass, name))
			element.setAttribute(name, objectToStringConvertor.toString(value));
		else
			parseElement(element, containingClass, name, value, parent, false);
	}
	
	private void parseElement(Element element, Class<?> containingClass, String name, Object value, Object parent, boolean isRecursive) {
		if (value.getClass().isArray()) {
			element.addContent(createListElement(Arrays.asList((Object[])value), name, containingClass, parent, isRecursive));
		} else if (value instanceof Iterable) {
			element.addContent(createListElement((Iterable<?>)value, name, containingClass, parent, isRecursive));
		} else if (value instanceof Map) {
			element.addContent(createMapElement((Map<?,?>)value, name, containingClass, parent, isRecursive));
		} else {
			parseSingleElement(element, containingClass, name, value, parent);
		}
	}
	
	private Element createMapElement(Map<?,?> map, String name, Class<?> containingClass, Object parent, boolean isRecursive) {
		Element mapElement = new Element(name);
		if (isRecursive) addClassNameAttribute(mapElement, map.getClass());
		for (Entry<?,?> entry : map.entrySet()) {
			Element entryElement = new Element(namingConvention.getMapItemTag(containingClass, name));
			parseElement(entryElement, containingClass, namingConvention.getMapKeyTag(containingClass, name), entry.getKey(), parent, true);
			parseElement(entryElement, containingClass, namingConvention.getMapValueTag(containingClass, name), entry.getValue(), parent, true);
			mapElement.addContent(entryElement);
		}
		return mapElement;
	}
	
	private Element createListElement(Iterable<?> iterable, String name, Class<?> containingClass, Object parent, boolean isRecursive) {
		Element listElement = new Element(name);
		if (isRecursive) addClassNameAttribute(listElement, iterable.getClass());
		for (Object item : iterable) if (item != null) 
			parseElement(listElement, containingClass, namingConvention.getListItemTag(containingClass, name), item, parent, true);
		return listElement;
	}
	
	private void parseSingleElement(Element element, Class<?> containingClass, String name, Object value, Object parent) {
		if (fieldPrimitiveStrategy.isPrimitive(containingClass, name, value)) {
			Element primitiveElement = new Element(name);
			primitiveElement.setText(objectToStringConvertor.toString(value));
			element.addContent(primitiveElement);
		} else {
			processElement(element, name, value, parent);
		}
	}
	
	private void addClassNameAttribute(Element element, Class<?> clazz) {
		if (configuration.getMarshallStyle() == MarshallStyle.BIDIRECTIONAL)
			element.setAttribute(namingConvention.getClassNameAttribute(), clazz.getName());
	}
}
